﻿// Search in Folders
// (c) 2023-2024 Christian Arellano García

g_label_name = "Search in Folders";
// Called by Directory Opus to initialize the script
function OnInit(initData) {
	initData.name = g_label_name;
	initData.version = "1.0.4";
	initData.copyright = "(c) 2023-2024 Christian Arellano García";
	initData.url = "https://resource.dopus.com/t/search-in-folders/46952";
	initData.desc = "Search in folders based on a user defined pattern";
	initData.default_enable = true;
	initData.min_version = "13.0.55";
	initData.config_desc = DOpus.Create.Map();
	initData.config.debug = DOpus.Create.Vector(1, 'ALL', 'WARNING', 'OFF');
	initData.config_desc('debug') = DOpus.strings.Get('debug');
	initData.config.delete_onstartup = false;
	initData.config_desc('delete_onstartup') = DOpus.strings.Get('delete_onstartup');
}
// Called when Directory Opus starts up
function OnStartup(startupData) {
	if (Script.config.delete_onstartup)
		DeleteColl();
}

// Called to add commands to Opus
function OnAddCommands(addCmdData) {
	var cmd = addCmdData.AddCommand();
	cmd.name = "SearchFolders";
	cmd.method = "OnSearchFolders";
	cmd.desc = "Search in selected/given folders based on a user defined pattern";
	cmd.label = g_label_name;
	cmd.template = "IN/K/M,PATTERN/R/O,SEP/K,CASE/S,REGEX/S,NOPARTIAL/S,NOWILD/S,NODIACRITICS/S,FOLDERS/S,EVERYTHING/S,VALUE/K[desc,contains,tags],NORECURSE/S";
	cmd.hide = false;
	cmd.icon = "script";
}

// Implement the SearchFolders command
function OnSearchFolders(scriptCmdData) {
	if (Script.config.debug) DOpus.ClearOutput();
	//=== INIT VARS ===================================================================================
	var s_tab = scriptCmdData.func.sourcetab;
	//if no tab(empty tab) or path doesn't exists, can't continue
	var util = DOpus.FSUtil();
	if (!s_tab || !util.Exists(s_tab.path)) {
		Log(3, "Tab is empty or path is not valid!");
		return;
	}
	//all the arguments received
	var args = scriptCmdData.func.argsmap;

	//check for IN argument existence
	if (args.Exists('IN')) {
		Log(1, "IN arguments was used. Checking for valid paths");
		var item;
		var dirs = DOpus.Create.Vector();
		//check for valid paths
		for (var i = 0; i < args('IN').count; i++) {
			try {
				item = util.GetItem(args('IN')(i));
				if (item.is_dir)
					dirs.push_back(item);
			}
			catch (e) {
				Log(2, "Can't validate " + args('IN')(i));
			}
		}
		item = null;
	}
	//no IN arg, so we're going to use selected folders
	else var dirs = s_tab.selected_dirs;

	//no folders selected, can't continue
	if (!dirs.count) {
		Log(3, "At least one folder is needed for search!");
		return;
	}

	//can't continue without a pattern to search for, show a MultiBox to load content
	if (!args.Exists('PATTERN') || args('PATTERN') == true) {
		Log(1, "The pattern is blank. Enter a valid pattern");
		args('PATTERN') = MultiBox(s_tab, s_tab.path);
	}
	if (!args('PATTERN')) {
		Log(3, "The pattern can't be empty!");
		return;
	}
	//=== CHECK ARGS/OPTIONS ============================================================================
	var incl_folders = (args.Exists('FOLDERS'));
	var val;
	if (args.Exists('VALUE'))
		val = args('VALUE').toLowerCase().split(',')[0].trim();
	if (!val) val = 'name';

	var CASE = (args.Exists('CASE'));
	var REGEX = (args.Exists('REGEX'));
	var NOWILD = (args.Exists('NOWILD'));
	var NOPARTIAL = (args.Exists('NOPARTIAL'));
	var NODIACRITICS = (args.Exists('NODIACRITICS'));
	var EVERYTHING = (args.Exists('EVERYTHING') && val == 'name') ? true : false;
	var RECURSE = args.Exists('NORECURSE') ? "RECURSE=no " : "";

	//=== CHECK EVERYTHING PROCESS ====================================================================
	util = DOpus.Create.SysInfo();
	if (EVERYTHING && !util.FindProcess("Everything64.exe") && !util.FindProcess("Everything.exe")) {
		Log(2, "Everything is not running. That feature can't be enabled");
		EVERYTHING = false;
	}

	Log(1, "cmdline     :" + scriptCmdData.cmdline);
	Log(1, "SOURCE TAB  :" + s_tab.path);
	Log(1, "FOLDERS     :" + dirs.count);
	Log(1, 'PATTERN     :"' + args('PATTERN') + '"');
	Log(1, "EVERYTHING  :" + EVERYTHING + '');
	Log(1, "SEARCH FOR  :" + val);
	Log(1, "RECURSE     :" + (RECURSE ? "false" : "true"));

	//=== BUILDING FILTER ============================================================================
	//if no SEP provided, use a Windows line break
	var sep = args.Exists('SEP') ? args('SEP') : '\r\n';
	var pattern = args('PATTERN').split(sep);
	var filter_text = '';
	if (EVERYTHING) {
		for (var i = 0; i < pattern.length; i++) {
			pattern[i] = pattern[i].trim();
			if (pattern[i] == '') continue;
			filter_text += '|' + '"' + pattern[i] + '"';
		}
		filter_text = filter_text.slice(1); //drop the first '|'
		//this line is not needed if folders are included in search
		if (!incl_folders) filter_text = 'file:' + filter_text;
		var dirs_text = 'path:';
		for (var i = 0; i < dirs.count; i++)
			dirs_text += '"' + dirs[i] + '"|';
		dirs_text = dirs_text.slice(0, -1);
	}
	else {
		for (var i = 0; i < pattern.length; i++) {
			pattern[i] = pattern[i].trim();
			if (pattern[i] == '') continue;
			filter_text += (!filter_text ? '' : ' or ') + val + ' match "' + pattern[i] + '"';
			filter_text += (NODIACRITICS ? ' ignorediacritics' : '');
			filter_text += (CASE ? ' matchcase' : '');
			filter_text += (REGEX ? ' regex' : '');
			filter_text += ((!/\*/g.test(pattern[i]) && NOWILD && !REGEX) ? ' nowild' : '');
			filter_text += (NOPARTIAL ? '' : ' partial');
		}
		//this line is not needed if folders are included in search
		if (!incl_folders) filter_text = 'type match files and match (' + filter_text + ')';

		//only used to check if filter is valid in DOpus
		var filter = DOpus.Create.Filter(filter_text);
		if (!filter.valid) {
			Log(3, "Filter can't be parsed : " + filter_text);
			return;
		}
		var dirs_text = '';
		for (var i = 0; i < dirs.count; i++)
			dirs_text += '"' + dirs[i] + '" ';
		filter = null;
	}
	Log(1, "FILTER      :" + filter_text);

	//=== BUILDING COMMAND LINE ========================================================================
	var collname = 'coll://Search in Folders/' + DOpus.Create().Date().Format('D#yyyy-MM-dd T#HH;mm;ss');
	var cmd = DOpus.Create.Command();
	var cmdline = 'FIND SHOWRESULTS=source,tab COLLNAME="' + collname + '" ';
	cmdline += EVERYTHING ? ('QUERYENGINE=everythingglobal' + (NOPARTIAL ? ',wholeword' : '') +
			(NODIACRITICS ? ',nodiacritics' : '') + (CASE ? ',case' : '') + (REGEX ? ',regexp' : '') + ' QUERY ' + dirs_text + " " + filter_text) :
		('IN ' + dirs_text + RECURSE + 'FILTERDEF ' + filter_text);
	Log(1, "COMMAND     :" + cmdline);
	try {
		cmd.RunCommand(cmdline);
	}
	catch (err) {
		Log(3, "Error running that command!");
	}
	if (cmd.results.result != 0) {
		util = DOpus.Create.StringTools();
		try {
			cmd.RunCommand('SetAttr "' + collname + '" DESCRIPTION "FOLDERS: ' + util.MakeLegal(dirs_text + '; PATTERN: ' + filter_text) + '"');
		}
		catch (err) {
			Log(2, "Error when writing search info: " + err);
		}
	}
	Log(1, "Search completed!");
	cmd = null;
	args = null;
	dirs = null;
	pattern = null;
	util = null;
}

function MultiBox(tab, start_path) {
	Log(1, "Starting dialog...");
	var msg, file, content;
	var dlg = tab.Dlg();
	dlg.title = g_label_name + ' - Pattern:';
	dlg.disable_window = tab.lister;
	dlg.template = 'multibox';
	dlg.detach = true;
	dlg.Create();
	dlg.AddHotkey("load_button_key", "ctrl+o");
	dlg.AddHotkey("ok_button_key", "ctrl+enter");
	dlg.Show();
	while (true) {
		msg = dlg.GetMsg();
		if (!msg.result) break;
		//Log(msg.control + "; " + msg.event);
		if (msg.event == "hotkey" && msg.Control === "ok_button_key") dlg.EndDlg(1);
		if ((msg.event === "click" && msg.Control === "load_button") || (msg.event == "hotkey" && msg.Control === "load_button_key")) {
			file = dlg.Open(g_label_name + ' - Open text file...', start_path);
			if (file.result) {
				content = ReadFile(String(file));
				if (content) {
					dlg.control("input_box").value = content.replace(/ +/g, ' ');
					dlg.control("OK_button").enabled = true;
				}
			}
		}
		else if (msg.event === "editchange" && msg.Control === "input_box") {
			if (dlg.control("input_box").value !== "") dlg.control("OK_button").enabled = true;
			else dlg.control("OK_button").enabled = false;
		}
	}
	content = '';
	if (dlg.result == 1)
		content = dlg.control("input_box").value;
	content = content.replace(/ +/g, ' ');
	content = content.replace(/\\/g, '\\\\');
	content = content.trim();
	dlg = null;
	return content;
}

function ReadFile(filename) {
	var res = false;
	var FSU = DOpus.FSUtil();
	var format = ["utf-8", "auto"];
	if (!FSU.Exists(filename))
		return res;
	var fh = FSU.OpenFile(filename);
	if (fh.error !== 0)
		Log(3, "An error occurred while opening file:" + filename);
	else {
		try {
			var blob = fh.Read();
			for (var i = 0; i < format.length; i++) {
				try {
					res = DOpus.Create.StringTools.Decode(blob, format[i]);
					if (res) break;
				}
				catch (e) {
					// Log(2, "Can't read " + filename + " as " + format[i]);
				}
			}
			blob.Free();
		}
		catch (e) {
			Log(3, "Can't read " + filename + ": " + e.description);
		}
	}
	fh.Close();
	FSU = null;
	return res;
}

function Log(level, text) {
	if (Script.config.debug < level || level == 3) {
		if (level == 1) DOpus.Output("INFO    => " + text);
		else if (level == 2) DOpus.Output("WARNING => " + text);
		else DOpus.Output("ERROR   => " + text, true);
	}
}

String.prototype.trim = function() {
	return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};

//Delete Everything collection
function DeleteColl() {
	try {
		var cmd = DOpus.Create.Command();
		Log(1, 'Deleting saved Search in Folders collections');
		cmd.RunCommand('Delete FILE="coll://Search in Folders" QUIET');
	}
	catch (e) {
		Log(2, "Can't delete coll://Search in Folders: " + e.description);
	}
}

==SCRIPT RESOURCES
<resources>
	<resource name="multibox" type="dialog">
		<dialog fontsize="9" height="213" lang="english" resize="yes" width="282">
			<control halign="left" height="186" multiline="yes" name="input_box" resize="wh" type="edit" width="270" x="6" y="6" />
			<control default="yes" height="14" name="load_button" resize="xy" title="Load Text File" type="button" width="56" x="165" y="195" />
			<control close="1" enable="no" height="14" name="OK_button" resize="xy" title="OK" type="button" width="50" x="225" y="195" />
		</dialog>
	</resource>
	<resource type="strings">
		<strings lang="english">
			<string id="debug">Logging level to be displayed. OFF to shows none. 
ALL to shows all the messages. 
WARNING to shows error conditions that are of consideration.</string>
			<string id="delete_onstartup">If true, all result collections will be deleted after starting DOpus.
It only affects collections created by this command.</string>
		</strings>
	</resource>
</resources>
